# Copyright (c) 2018 Presto Labs Pte. Ltd.
# Author: jshin

import collections
import re
import warnings
from typing import Union, List

from coin.exchange.base.kr_rest.py_product import PyProduct
from coin.base.param_util import to_list
from coin.exchange.base.kr_rest.futures_product import NormFuturesProduct
from coin.exchange.base.kr_rest.product import NormProduct

market_type_list = ('ExWallet', 'Spot', 'Margin', 'Futures', 'Options', 'Info')

_SubscriptionRequest = collections.namedtuple('SubscriptionRequest',
                                              ['market_type', 'exchange', 'api_version'])


class SubscriptionRequest(_SubscriptionRequest):
  # override __new__, not __init__
  # https://stackoverflow.com/questions/42385916/inheriting-from-a-namedtuple-base-class-python
  def __new__(cls, market_type, exchange, api_version=None):
    assert market_type in market_type_list, market_type
    assert exchange is not None, exchange
    self = super(SubscriptionRequest, cls).__new__(cls, market_type, exchange, api_version)
    return self

  def __str__(self):
    return "SubReq(%s,%s,%s)" % (self.market_type, self.exchange, self.api_version)

  def __repr__(self):
    return "SubReq(%s,%s,%s)" % (self.market_type, self.exchange, self.api_version)

  def is_compatible(self, other):
    # do not compare api_version
    return (type(other) == SubscriptionRequest and other.market_type == self.market_type and
            other.exchange == self.exchange)

  def to_str(self):
    if self.api_version is None:
      return '%s.%s' % (self.market_type, self.exchange)
    else:
      return '%s.%s.%s' % (self.market_type, self.exchange, self.api_version)

  @staticmethod
  def _from_str_1(request_str):
    if request_str == 'OkexFutures':
      return SubscriptionRequest('Futures', 'Okex', None)

    splitted = request_str.split('_', 1)
    exchange = splitted[0]
    api_version = None
    if len(splitted) > 1:
      api_version = splitted[1]
    return SubscriptionRequest('Spot', exchange, api_version)

  @staticmethod
  def _from_str_2(request_str):
    assert isinstance(request_str, str), type(request_str)
    # Use named group extension for easy processing.
    # Valid request str example: Spot.Bithumb.v1, Futures.Okex, Margin.Bitfinex.v2
    pattern = (r'^(?P<market_type>Spot|Margin|Futures|Options)\.(?P<exchange>[A-Za-z0-9]+)'
               r'(\.(?P<api_version>[0-9a-z_\.\-]+))?$')
    match = re.match(pattern, request_str)
    if match:
      match_dict = match.groupdict(None)
      assert match['market_type'], match_dict
      assert match['exchange'], match_dict
      return SubscriptionRequest(**match_dict)
    else:
      raise ValueError('Invalid subscription string! %s' % request_str)

  @staticmethod
  def from_str(request_str):
    assert isinstance(request_str, str), type(request_str)

    try:
      sub_key = SubscriptionRequest._from_str_2(request_str)
      return sub_key
    except (AssertionError, ValueError):
      pass

    # For backward compatibility
    try:
      pattern = '^[A-Za-z0-9]+(_v[0-9]+)?$'
      assert re.match(pattern, request_str)
      sub_key = SubscriptionRequest._from_str_1(request_str)
      warnings.warn('Deprecated Subscription string (%s).' % request_str)
      return sub_key
    except AssertionError:
      pass

    raise ValueError('Invalid subscription string! %s' % request_str)

  @staticmethod
  def from_dict(request_dict):
    market_type = request_dict.get('market_type', 'Spot')
    exchange = request_dict.get('exchange', None)
    api_version = request_dict.get('api_version', None)
    if market_type == 'Futures' and exchange == 'OkexFutures':
      exchange = 'Okex'
    return SubscriptionRequest(market_type, exchange, api_version)

  @staticmethod
  def from_any(request):
    if type(request) == SubscriptionRequest:
      return request
    elif type(request) == str:
      return SubscriptionRequest.from_str(request)
    elif type(request) == dict:
      return SubscriptionRequest.from_dict(request)
    else:
      raise ValueError("not supported", request)


def get_subscription_key(product, *, use_margin_account=False, api_version_override_map=None):
  if api_version_override_map is None:
    api_version_override_map = {}
  assert isinstance(product, (NormProduct, NormFuturesProduct, PyProduct)), type(product)
  if product.product_type == "Spot":
    if use_margin_account:
      market_type = "Margin"
    else:
      market_type = "Spot"
  elif product.product_type == "Futures":
    market_type = "Futures"
  elif product.product_type == "Options":
    market_type = "Options"
  else:
    raise ValueError(product.product_type)

  if (product.product_type == 'Spot'
      and (product.exchange == 'Binance' or product.exchange == 'Binanceus')):
    sub_key = SubscriptionRequest(
        market_type,
        product.exchange,
        api_version_override_map.get((market_type, product.exchange), 'snapshot'))
  elif (product.product_type == 'Futures'
        and product.exchange == 'Binance'
        and product.quote.currency == 'USD'):
    sub_key = SubscriptionRequest('Futures', 'Binance', 'v1-delivery.l1_realtime_trade_move2bp')
  elif (product.product_type == 'Futures'
        and product.exchange == 'Okex'
        and "PERPETUAL" in product.symbol):
    sub_key = SubscriptionRequest('Futures', 'Okex', 'v5_swap')
  elif (product.product_type == 'Futures'
        and product.exchange == 'Huobi'
        and "PERPETUAL" in product.symbol):
    if product.quote.currency == 'USDT':
      sub_key = SubscriptionRequest('Futures', 'Huobi', 'v1-linear-swap.bbo')
    else:
      sub_key = SubscriptionRequest('Futures', 'Huobi', 'v1-swap.bbo')
  elif (product.product_type == 'Futures'
        and product.exchange == 'Bybit'
        and "PERPETUAL" in product.symbol):
    if product.quote.currency == 'USDT':
      sub_key = SubscriptionRequest('Futures', 'Bybit', 'v2-linear.realtime')
    else:
      sub_key = SubscriptionRequest('Futures', 'Bybit', 'v2.realtime')
  else:
    sub_key = SubscriptionRequest(
        market_type, product.exchange,
        api_version_override_map.get((market_type, product.exchange), None))
  return sub_key


class FeedSubscriptionRequest(object):
  def __init__(self):
    self._sub_request_to_products_map = {}

  def add_products(self, products, sub_request):
    for product in products:
      assert product.exchange == sub_request.exchange
    existing_products = self._sub_request_to_products_map.get(sub_request, [])
    existing_products.extend(products)
    existing_products = list(dict.fromkeys(existing_products))
    self._sub_request_to_products_map[sub_request] = existing_products

  def get_products(self, sub_request):
    return self._sub_request_to_products_map.get(sub_request, [])

  def merge_from(self, feed_sub_request):
    for sub_req, products in feed_sub_request._sub_request_to_products_map.items():
      self.add_products(products, sub_req)

  @property
  def all_products(self):
    all_products = []
    for sub_request, products in self._sub_request_to_products_map.items():
      all_products.extend(products)
    return all_products

  def gen_iter(self):
    for sub_request, products in self._sub_request_to_products_map.items():
      yield sub_request, products

  @staticmethod
  def create(products, sub_request):
    feed_sub_request = FeedSubscriptionRequest()
    feed_sub_request.add_products(products, sub_request)
    return feed_sub_request

  @staticmethod
  def from_multi(sub_requests):
    ret = FeedSubscriptionRequest()
    valuekeymap = {}
    for sub_request in sub_requests:
      for key, prods in sub_request._sub_request_to_products_map.items():
        for prod in prods:
          if prod in valuekeymap:
            assert valuekeymap[prod] == key, (prod, valuekeymap[prod], key)
          valuekeymap[prod] = key

    for value, key in valuekeymap.items():
      if key not in ret._sub_request_to_products_map:
        ret._sub_request_to_products_map[key] = []
      ret._sub_request_to_products_map[key].append(value)
    print("all sub request goes along well")
    print(ret._sub_request_to_products_map)
    return ret


def get_feed_sub_request(feed_sub_request_or_products: Union[FeedSubscriptionRequest,
                                                             List[NormProduct]]):
  if isinstance(feed_sub_request_or_products, FeedSubscriptionRequest):
    return feed_sub_request_or_products
  else:
    products = to_list(feed_sub_request_or_products)
    feed_sub_request = FeedSubscriptionRequest()
    for product in products:
      feed_sub_request.add_products([product], get_subscription_key(product))
    return feed_sub_request


def gen_feed_sub_request_with_prefer(products, prefer_subreq):
  assert prefer_subreq is None or isinstance(prefer_subreq, SubscriptionRequest)
  feed_sub_request = FeedSubscriptionRequest()
  for product in to_list(products):
    prefer_chosen = False
    if (prefer_subreq is not None and product.exchange == prefer_subreq.exchange
        and ((product.product_type == 'CurrencyPair' and prefer_subreq.market_type == 'Spot') or
             (product.product_type == 'Futures' and prefer_subreq.market_type == 'Futures') or
             (product.product_type == 'Spot') and prefer_subreq.market_type == 'Spot')):
      if prefer_subreq == SubscriptionRequest('Futures', 'Okex', 'v5') and product.is_perpetual:
        pass
      elif prefer_subreq == SubscriptionRequest('Futures', 'Okex',
                                                'v5_swap') and not product.is_perpetual:
        pass
      else:
        prefer_chosen = True
        feed_sub_request.add_products([product], prefer_subreq)
    if not prefer_chosen:
      feed_sub_request.add_products([product], get_subscription_key(product))
  return feed_sub_request
